//
//  DerbyNSOde.m
//  DerbyOSSampler
//
//  Created by glenn andreas on 1/6/14.
//  Copyright (c) 2014 glenn andreas. All rights reserved.
//

#import "DerbyNSOde.h"
#import "DerbyNSOdeResolver.h"
#import "DerbyNSOdePeer.h"
#import "DerbyNSOdeAddress.h"
#import "zmq.h"
#import "zmq_utils.h"

#import "DXMLObject.h"
#import "DXMLImporter.h"
#import "DerbyNSOdeLog.h"

#import "DXMLBoutObject.h"
#import "DXMLPeriodObject.h"
#import "DXMLJamObject.h"

#import "DerbyBoutState.h"

NSString *DerbyNSOdePEER = @"PEER"; // body contains address:port of new peer
NSString *DerbyNSOdeHELO = @"HELO"; // body contains address:port of sender
NSString *DerbyNSOdeNAME = @"NAME"; // body contains current user visible name
NSString *DerbyNSOdeCHAT = @"CHAT"; // body contains arbitrary text
NSString *DerbyNSOdeXMSG = @"XMSG"; // body contains XML
NSString *DerbyNSOdeGBYE = @"GBYE"; // body is empty/ignored

NSString *DerbyNSOdeDNSTYPE = @"_nsode_derbyos_com._tcp.";

@interface DerbyNSOde()<DerbyNSOdeResolverClient>
@property (strong) DerbyNSOdeResolver *resolver;
@property (assign) void *ctx;
@property (assign) void *inbox;
@property (strong) NSString *bout;
@property (strong) NSString *name;
@property (strong) NSMutableArray *peers;
@property (strong) NSMutableDictionary *peerMap;
@property (strong) NSString *uuid;
@property (assign) int port;
@property (assign, atomic) BOOL polling;
@end

@implementation DerbyNSOde
- (void) dealloc
{
    zmq_close(_inbox);
    zmq_ctx_destroy(_ctx);
}
- (id) initWithContext: (void *) ctx
{
    if (!ctx) {
        ctx = zmq_ctx_new();
    }
    self = [super init];
    if (self) {
        _ctx = ctx;
        _bout = @"";
        _inbox = zmq_socket(_ctx, ZMQ_ROUTER);
        zmq_bind(_inbox, "tcp://*:*"); // random port, all available interfaces
        char buffer[1024];
        size_t optvalen = 1024;
        zmq_getsockopt(_inbox, ZMQ_LAST_ENDPOINT, buffer, &optvalen);
        NSString *fullAddr = [NSString stringWithFormat:@"%s",buffer];
        _port = [[[fullAddr componentsSeparatedByString:@":"] lastObject]intValue];
        _peers = [NSMutableArray array];
        _peerMap = [NSMutableDictionary dictionary];
        CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
        _uuid = (__bridge_transfer NSString *)CFUUIDCreateString(kCFAllocatorDefault, uuid);
        CFRelease(uuid);
        _name = [NSString stringWithFormat:@"NSO%u", arc4random() % 0xfffff]; // yes, arc4random is overkill
        _resolver = [[DerbyNSOdeResolver alloc] initWithRegistrationType:DerbyNSOdeDNSTYPE delegate:self];
        [_resolver broadcastIdentity:_name port:_port uuid:_uuid boutId:_bout];
        _xml = [DXMLBoutObject defaultBout];
        _boutState = [[DerbyBoutState alloc] initWithBout:_xml];
    }
    return self;
}

- (DerbyNSOdePeer *) findPeerFromAddress: (DerbyNSOdeAddress *) address uuid: (NSString *) uuid
{
    for (DerbyNSOdePeer *peer in self.peers) {
        if (([peer.address isEqual:address]) || ([uuid isEqualToString:peer.uuid])) {
            //            peerLogger.debug("Skipping adding peer %s:%d, already there", address, port)
            return peer; // already found as a peer
        }
    }
    return nil;
}
- (DerbyNSOdePeer *) addPeer: (DerbyNSOdeAddress *) address uuid: (NSString *) uuid introducedBy: (DerbyNSOdePeer *) introducer
{
    DerbyNSOdePeer *peer = [self findPeerFromAddress:address uuid:uuid];
    if (peer) {
//            peerLogger.debug("Skipping adding peer %s:%d, already there", address, port)
            return peer; // already found as a peer
    }
//    peerLogger.debug("Adding peer %s:%d %s", address, port, uuid)
    peer = [[DerbyNSOdePeer alloc] initWithContext:self.ctx address:address uuid:uuid];
    [self.peers addObject:peer];
    NSError *error;
    if (![peer connect:&error]) {
        [self.peers removeObject:peer];
        return nil;
    }
    if (uuid) {
        peer.uuid = uuid;
//        peerLogger.debug("Created peer %s",peer)
// be polite and introduce our new peer to every other peer
// (and those peers will then introduce themselves if they
// haven't already)
        for (DerbyNSOdePeer *other in self.peers) {
            if (other != peer && other != introducer) {
                [other tellPeer:DerbyNSOdePEER body:@""]; // '%s:%d' % (address, port))
            }
        }
    }
    [self didChangeValueForKey:@"peers"];
    return peer;
}
- (void) removePeer: (DerbyNSOdePeer *) peer reason: (NSString *) reason
{
    if ([self.peers containsObject:peer]) {
//        peerLogger.debug("Removing Peer %s",peer)
        if (!reason) reason = @"";
        [peer tellPeer:DerbyNSOdeGBYE body:reason];
        [peer disconnect];
        [self.peers removeObject:peer];
        for (id key in [self.peerMap allKeysForObject:peer]) {
            [self.peerMap removeObjectForKey:key];
        }
    }
}
- (void) addPeer:(NSNetService *)service uuid:(NSString *)uuid
{
    LOG_INFO(PEER,@"Add peer %@: %@", uuid, service);
    DerbyNSOdeAddress *address = nil;
    NSArray *addresses = [service addresses];
    if (addresses.count ==0) {
        LOG_WARN(PEER,@"No address for peer, not adding");
        return;
    } else if (addresses.count > 1) {
        // multiple addresses - which do we use?
        address = [[DerbyNSOdeAddress alloc] initWithData:addresses[0]];
    } else {
        address = [[DerbyNSOdeAddress alloc] initWithData:addresses[0]];
    }
    [self addPeer:address uuid:uuid introducedBy:nil];
}
- (void) removePeer:(NSNetService *)service uuid:(NSString *)uuid
{
    LOG_INFO(PEER,@"Remove peer %@: %@", uuid, service);
    DerbyNSOdePeer *peer = nil;
    DerbyNSOdeAddress *address = nil;
    NSArray *addresses = [service addresses];
    if (addresses.count ==0) {
        LOG_WARN(PEER,@"No address for peer, not adding");
    } else if (addresses.count > 1) {
        // multiple addresses - which do we use?
        address = [[DerbyNSOdeAddress alloc] initWithData:addresses[0]];
    } else {
        address = [[DerbyNSOdeAddress alloc] initWithData:addresses[0]];
    }
    peer = [self findPeerFromAddress: address uuid: uuid];
    if (peer) {
        [self removePeer:peer reason:@"mDNS vanished"];
    }
}

#pragma mark XML tree
- (void) askForUpdates: (DerbyNSOdePeer *) peer
{
    // not yet
}

#pragma mark Sending Messages
- (void) chat: (NSString *) msg
{
    [self tellAllPeers:DerbyNSOdeCHAT body:msg];
}
- (void) sendXML: (DXMLObject *) xml
{
    if (xml) {
        NSString *body = xml.xmlString;
        if (body) {
            [self tellAllPeers:DerbyNSOdeXMSG body:body];
        }
    }
}
- (void) sendXML: (DXMLObject *) xml toPeer: (DerbyNSOdePeer *) peer
{
    if (xml) {
        NSString *body = xml.xmlString;
        if (body) {
            [peer tellPeer:DerbyNSOdeXMSG body:body];
        }
    }
}
- (void) tellAllPeers: (NSString *) msg body: (NSString *) body
{
    for (DerbyNSOdePeer *peer in self.peers) {
        [peer tellPeer:msg body:body];
    }
}

#pragma mark Receiving Messages
- (void) handleHelloMessage: (NSString *) uuid from:(DerbyNSOdeAddress *) address
{
//# an HELO message may not have an existing peer - if there isn't
//# one, add as a peer.  If there is one, make sure its id matches
    DerbyNSOdePeer *peer = [self findPeerFromAddress: address uuid: uuid];
    if (peer) {
        if ([uuid isEqualToString:self.uuid]) {
//            msgLogger.debug("Talking to myself!")
            [self removePeer:peer reason:@"Talking to myself"];
        }
        peer.uuid = uuid;
//        peerLogger.info("Met %s", peer)
    } else {
        // a stranger greeted us
        peer = [self addPeer:address uuid:uuid introducedBy:nil];
    }
    if (self.name) {
        [peer tellPeer:DerbyNSOdeNAME body:self.name];
    }
    [self askForUpdates:peer];
}

- (void) handlePeerMessage: (DerbyNSOdePeer *) peer body: (NSString *) body
{
    LOG_DBG(PEER,@"%@ peer: %@", peer, body);
//# the peer is the person who sent us the introduction, not the
//# node being introduced (which is in the body)
    DerbyNSOdeAddress *addr = [[DerbyNSOdeAddress alloc] initWithString:body];
//# don't bother to tell the peer who introduced us about the new peer
    [self addPeer:addr uuid:nil introducedBy:peer];
}
- (void) handleChatMessage: (DerbyNSOdePeer *) peer body: (NSString *) body
{
    LOG_DBG(MSG,@"%@ says: %@", peer, body);
}
- (void) handleGoodbyeMessage: (DerbyNSOdePeer *) peer body: (NSString *) body
{
    LOG_DBG(MSG,@"goodbye %@ (%@)",peer, body);
    [peer disconnect];
    [self.peers removeObject:peer];
}
- (void) handleNameMessage: (DerbyNSOdePeer *) peer body: (NSString *) body
{
    LOG_DBG(MSG,@"%@ new name %@",peer, body);
    peer.serviceName = body;
}
- (void) handleXMLMessage: (DerbyNSOdePeer *) peer body: (NSString *) body
{
    LOG_DBG(MSG,@"%@ xml: %@",peer, body);
    DXMLImporter *importer = [[DXMLImporter alloc] initWithData:[body dataUsingEncoding:NSUTF8StringEncoding]];
    NSError *error = nil;
    DXMLObject *root = [importer parse:&error];
    if (root) {
        [self handleXML: root fromPeer: peer];
    }
}
- (void) handleMessage: (NSString *) msg body: (NSString *) body fromPeer: (DerbyNSOdePeer *) peer
{
    LOG_DBG(XML,@"msg from peer %@: %@/%@", peer,msg,body);
    if ([msg isEqualToString:DerbyNSOdePEER]) {
        [self handlePeerMessage:peer body:body];
    } else if ([msg isEqualToString:DerbyNSOdeCHAT]) {
        [self handleChatMessage:peer body:body];
    } else if ([msg isEqualToString:DerbyNSOdeGBYE]) {
        [self handleGoodbyeMessage:peer body:body];
    } else if ([msg isEqualToString:DerbyNSOdeNAME]) {
        [self handleGoodbyeMessage:peer body:body];
    } else if ([msg isEqualToString:DerbyNSOdeXMSG]) {
        [self handleXMLMessage:peer body:body];
    }
}
- (void) receiveMessages
{
    NSMutableArray *parts = [NSMutableArray array];
    while (1) {
        zmq_msg_t msg;
        zmq_msg_init(&msg);
        if (zmq_recvmsg(self.inbox, &msg, 0)>=0) {
            // since we are a router
            char *cmd = zmq_msg_data(&msg);
            size_t len = zmq_msg_size(&msg);
            NSData *data = [NSData dataWithBytes:cmd length:len];
            [parts addObject:data];
        }
        BOOL more = zmq_msg_more(&msg);
        zmq_msg_close(&msg);
        if (!more) break;
    }
    // parts is now an array of data messages
    NSString *senderId = nil;
    if ([parts count]) {
        if (1) { // inbox.socket_type == ROUTER
            NSData *data = [parts objectAtIndex:0];
            // this is a 16 byte UUID
            NSAssert(data.length == sizeof(CFUUIDBytes), @"Received message where first item not UUID");
            CFUUIDBytes bytes;
            [data getBytes:&bytes];
            CFUUIDRef uuid = CFUUIDCreateFromUUIDBytes(kCFAllocatorDefault, bytes);
            senderId = (__bridge_transfer NSString *)CFUUIDCreateString(kCFAllocatorDefault, uuid);
            CFRelease(uuid);
            [parts removeObjectAtIndex:0];
        }
    }
    NSString *msg = nil;
    NSString *body = nil;
    NSData *bodyData = nil;
    if ([parts count]) {
        NSData *data = [parts objectAtIndex:0];
        msg = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        [parts removeObjectAtIndex:0];
        if ([parts count]) { //# for unknown reasons, we sometimes get the message and body in two messages
            bodyData = [parts objectAtIndex:0];
            body = [[NSString alloc] initWithData:bodyData encoding:NSUTF8StringEncoding];
            [parts removeObjectAtIndex:0];
        } else {
            LOG_ERR(MSG,@"Missing body from frame %@ %@",msg, senderId);
        }
    }
    if ([msg isEqualToString:DerbyNSOdeHELO]) {
        DerbyNSOdeAddress *addr = [[DerbyNSOdeAddress alloc] initWithString: body];
        [self handleHelloMessage:senderId from:addr];
    } else {
        DerbyNSOdePeer *peer = [self findPeerFromAddress:nil uuid:senderId];
        if (peer) {
            [self handleMessage: msg body: body fromPeer: peer];
        } else {
            LOG_ERR(MSG,@"Message from unknown peer %@",senderId);
        }
    }
}

#pragma mark XML Manipulation
- (DXMLObject *) findEID: (NSString *) eid inTree: (DXMLObject *) obj
{
    if ([obj.attributes[@"eid"] isEqualToString:eid]) {
        return obj;
    }
    for (DXMLObject *child in obj.children) {
        DXMLObject *retval = [self findEID:eid inTree:child];
        if (retval)
            return retval;
    }
    return nil;
}
- (DXMLObject *) findInTree: (NSDictionary *) attr
{
    NSString *path = [attr objectForKey:@"path"];
    if (path) {
        DXMLObject *retval = [self.xml find:path];
        if (retval)
            return retval;
        LOG_WARN(XML, @"Couldn't find path %@ in %@", path, self.xml.xmlString);
    }
    NSString *period = [attr objectForKey:@"period"];
    if (period){
        DXMLPeriodObject *periodXML = (DXMLPeriodObject *)[[(DXMLBoutObject *)self.xml periods] objectAtIndex:[period integerValue]-1];
        NSString *jam = [attr objectForKey:@"jam"];
        if (jam) {
            DXMLJamObject *jamXML = [periodXML.jams objectAtIndex: [jam integerValue]-1];
            return jamXML;
        } else {
            return periodXML;
        }
    }
    NSString *eid = [attr objectForKey:@"eid"];
    if (eid) {
        DXMLObject *retval = [self findEID:eid inTree:self.xml];
        if (retval)
            return retval;
        LOG_WARN(XML, @"Couldn't find eid %@ in %@", eid, self.xml.xmlString);
    }
    LOG_WARN(XML, @"Couldn't find %@ in %@", attr, self.xml.xmlString);
    return nil;
}

- (void) setXMLString: (NSString *) string inTree: (NSDictionary *) specifier
{
    DXMLObject *xml = [self findInTree:specifier];
    if (xml) {
        NSString *attr = specifier[@"attribute"];
        if (attr) {
            [xml setAttribute:string forKey:attr];
        } else {
            [xml setText:string];
        }
        [self.boutState updateBoutState:xml];
    } else {
        LOG_WARN(XML, @"SetXML couldn't find node %@", specifier);
    }
}
- (void) setXMLNode: (DXMLObject *) xml inTree: (NSDictionary *) specifier
{
}
- (void) clearXMLInTree: (NSDictionary *) specifier
{
    DXMLObject *xml = [self findInTree:specifier];
    if (xml) {
        NSString *attr = specifier[@"attribute"];
        if (attr) {
            [xml setAttribute:nil forKey:attr];
        } else {
            [xml setText:@""];
        }
        [self.boutState updateBoutState:xml];
    } else {
        LOG_WARN(XMLSET, @"clearXMLInTree couldn't find node %@", specifier);
    }
}
- (void) addXMLNode: (DXMLObject *) xml inTree: (NSDictionary *) specifier
{
    LOG_DBG(XMLADD, @"Adding %@ to %@",xml,specifier);
    DXMLObject *parent = [self findInTree:specifier];
    if (parent) {
        [parent addChild:xml];
        [self.boutState updateBoutState:xml];
    }
}

- (void) handleXML: (DXMLObject *) xml fromPeer: (DerbyNSOdePeer *) peer
{
    //    if xml.tag == "set":
    //        value = xml.text
    //        if value: # set text value
    //            self.SetXMLInTree(value, **xml.attrib)
    //        elif len(xml.getchildren()) > 0:
    //            self.SetXMLInTree(xml[0], **xml.attrib)
    //        else:
    //                self.SetXMLInTree(None, **xml.attrib)
    //     elif xml.tag == "add":
    //                self.AddXMLInTree(xml[0], **xml.attrib)
    //                elif xml.tag == "replace":
    //                self.lastUpdate = self.time.elapsedTime
    //#            self.ReplaceXMLInTree(xml[0], **xml.attrib)
    //                elif xml.tag == "remove":
    //                self.lastUpdate = self.time.elapsedTime
    //                elif xml.tag == "update":
    //                print "XML Update request received", ET.tostring(xml)
    //                updateTime = float(xml.text)
    //                if updateTime < self.lastUpdate:
    //                    print "We need to send an update to ", peer
    //#msgs = self.newerXMLSince(self.derbyXML, updateTime)
    //                    msg = ET.Element("replace", {"path":"./"})
    //                    msg.append(self.derbyXML._xml) #ugh!
    //                    self.sendXML(msg, peer)
    //#for msg in msgs:
    //#        print "Updating", ET.tostring(msg)
    //#        self.sendXML(msg, peer)
    //
    //                    xmlLogger.debug("xml message: %s",ET.tostring(xml))
    if ([xml.elementName isEqualToString:@"set"]) {
        NSString *value = [xml text];
        if (value.length) {
            [self setXMLString:value inTree:xml.attributes];
        } else if (xml.children.count) {
            [self setXMLNode:xml.children[0] inTree:xml.attributes];
        } else {
            [self clearXMLInTree:xml.attributes];
        }
    } else if ([xml.elementName isEqualToString:@"add"]) {
        [self addXMLNode:[xml.children lastObject] inTree: xml.attributes];
    } else if ([xml.elementName isEqualToString:@"replace"]) {
        
    } else if ([xml.elementName isEqualToString:@"remove"]) {
        
    } else if ([xml.elementName isEqualToString:@"update"]) {
        
    }
}

#pragma mark Background polling
- (void) poll: (NSTimeInterval) timeout
{
    zmq_pollitem_t items[1];
    items[0].socket = self.inbox;
    items[0].events = ZMQ_POLLIN;
    int poller = zmq_poll(items, 1, timeout * 1000);
    if (poller > 0) {
        if (items[0].revents & ZMQ_POLLIN)
            [self performSelectorOnMainThread:@selector(receiveMessages) withObject:nil waitUntilDone:YES];
    }
}
- (void) pollInBackground
{
    self.polling = YES;
    static NSOperationQueue *opq = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        opq = [NSOperationQueue new];
    });
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
        while (self.polling) {
            [self poll: 3];
        }
    }];
    [opq addOperation:op];
}
- (void) stopPolling
{
    self.polling = NO;
}
@end
